Python ジェネレータ - イテレータから協調型マルチタスクまで - 1
著者:Leonardo Giordani - 25/03/2013
はじめに
Pythonは11年の歴史の中で、非常に目覚しい発展を遂げた言語であり、いくつかの新機能が導入されました。これらの新機能は、時には他の言語から借用され、時には開発者のニーズから生まれ、公式に実装される前に大きく議論されました。これらの改良の1つはジェネレータに関するもので、これは70年代からコンピュータサイエンスの世界で見られる概念で、Pythonではバージョン2.2(2001)から実装され、バージョン2.3(2003)から普及しました。
ジェネレータは、関数を一般化したもので、反復や繰り返し実行など、プログラムの流れに関わるすべてのことを、より完全で豊かな方法で扱うことができます。ここ数年、時代遅れと思われていた概念が再び広まり始めました。それは、協調的マルチタスクの概念です。この概念は、マルチプロセッシングやマルチスレッドの出現により、何年か影を潜めていましたが、インタプリタ型言語や仮想マシンに起こったように、時間の経過や文脈の変化に伴い、良いアイデアが再び立ち上がり、死語ではないことが証明されました。
特にPythonの世界では、マイクロスレッドの使用を推奨する数多くのソリューションが登場しています。マイクロスレッドは、従来のプロセスやスレッドとは異なり、暗黙のスケジューリングを行わない並列実行フローです。このようなオブジェクトの大きな利点は、同期やデータ保護の問題がすべて存在しないため、マルチプログラミングコードの実装と管理が容易なことです。一方で、このようなオブジェクトを使用するには、自発的なスケジューリング、つまりシステムリソースの取得と解放を明示的に行うシステムが必要になります。
Pythonで協調型マルチタスクの話を始めるには、generatorを理解することが必須となります。この最初の記事では、イテレーションの概念とその実装について説明します。
イテレーション
Pythonのイテレーションは、他の言語と同様に、for文によって制御されるプロセスで、コードブロックを繰り返し実行し、与えられた順序付きセットから実行のたびに抽出された値を変数に割り当てることができます。反復処理の最も単純なケースは、値のリストを処理することです。
code: python
print i
しかし、Pythonでは、反復処理は、配列の要素を単純にループするだけではありません。for文は、非常に複雑なオブジェクトを構築することができる、よく定義された自明でないプロトコルを実装しています。
Pythonの反復処理の構造を理解するためには、イテラブルうオブジェクトとイテレータオブジェクトの違いを明確にする必要があります。
イテレータ
Pythonの専門用語では、イテレータ(Iterator) は次のような特性を持つオブジェクトです。
データのセットを含む
next()メソッドを公開しており、呼び出しのたびに含まれる要素の1つを返します。各要素は一度だけ返されます。このメソッドは、イテレータが取り込んだデータセット全体を通過します。Python 3では、このメソッドは__next__()と改名されました。
next()メソッドが最後の要素を返した後、このメソッドを連続して呼び出すとStopIteration例外が発生します。これはイテレータが使い果たされたことを意味します。
イテレータ自体を返す __iter__() メソッドを公開しています。
イテラブル
一方、イテラブル(Iterable)の定義は、より一般的なものです。イテレート可能は、__getitem__() または __iter__() メソッド(またはその両方)を公開するデータのコンテナです。
__getitem__(i) は、与えられた位置iの値を返すか、その位置にデータがない場合は IndexError 例外を発生させます。
__iter__() は、イテレートテーブルに含まれるデータのイテレータを返します。
ご覧のとおり、__getitem__()メソッドはデータを順序付きのセットとみなしますが、必ずしもそうではありません。このため、イテレートテーブルには2つの異なるメソッドを定義することができます。
前述の定義から、イテレータも自動的にイテラブルであることがわかります。なぜなら、イテレータ(自身)を返す __iter__() メソッドを公開しているからです。
ループプロトコル
上のループ構文に戻ると、Pythonではfor文の引数にイテレート可能なものを想定している、という問題を明確にすることができます。これはつまり、どんなオブジェクトでもforループで使用できる機能を与えることができるということです。先に述べた2つのメソッド、__getitem__() または __iter__() のいずれかを公開するだけです。
例を見てみましょう。
code: python
class AnIterator(object):
def __init__(self, value):
self.value = value
def next(self):
if self.value <= 0:
raise StopIteration
tmp = self.value
self.value = self.value - 1
return tmp
def __iter__():
return self
このオブジェクトは __iter__() を公開しているのでイテレータです。next() メソッドは、与えられた数から始まる整数の減少するシーケンスを返します。これをテストすると次のようになります。
code: python
>> iterator = AnIterator(3)
>> print iterator.next()
3
>> print iterator.next()
2
>> print iterator.next()
1
>> print iterator.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in next
StopIteration
>>
>> iterator = AnIterator(3)
>> for i in iterator:
... print i
...
3
2
1
この実行では、イテレータをforループで使用できることを示しています。next()の最初の3回の呼び出しで最初のインスタンスを使い切ってしまったため、クラスを2回インスタンス化しなければならなかったことに注目してください。
forループが実行されたときに何が起こるのか、もう少し詳しく見てみましょう。Pythonコード
code: python
for i in iterable:
some_code
これは次のコードと等価です。
code: python
_iter = iterable.__iter__()
while 1:
try:
i = _iter.next()
except StopIteration:
break
some_code
for構文は、イテレート可能なオブジェクトを受け取り、その__iter__()メソッドを呼び出してイテレータオブジェクトを取得し、StopIteration例外が発生するまで後者の next() メソッドを呼び出します。実際のコードは少し異なりますが、ここではわかりやすくするために簡略化しています。詳しく知りたい方は、以下のアドレスを参照してください。
まとめ
この最初の投稿では、Pythonでは多くの古典的な言語とは非常に異なる、for文によって実装されるループプロトコルを要約しようとしました。次の記事では、generator の概念とその Python の実装を探ります。
Part 1 of the Python generators - from iterators to cooperative multitasking series